/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.iec.edp.caf.rpc.client.grpc;

import io.iec.edp.caf.boot.context.CAFContext;
import io.iec.edp.caf.common.JSONSerializer;
import io.iec.edp.caf.commons.exception.CAFRuntimeException;
import io.iec.edp.caf.commons.exception.ExceptionLevel;
import io.iec.edp.caf.commons.exception.entity.DefaultExceptionProperties;
import io.iec.edp.caf.commons.exception.entity.ExceptionErrorCode;
import io.iec.edp.caf.commons.runtime.CafEnvironment;
import io.iec.edp.caf.commons.utils.SpringBeanUtils;
import io.iec.edp.caf.core.session.ICafSessionService;
import io.iec.edp.caf.msu.api.ServiceUnitAwareService;
import io.iec.edp.caf.msu.client.exception.ServiceUnitNotFoundException;
import io.iec.edp.caf.rpc.api.entity.RpcReturnValueDefinition;
import io.iec.edp.caf.rpc.api.entity.RpcServiceMethodDefinition;
import io.iec.edp.caf.rpc.api.grpc.service.GrpcClient;
import io.iec.edp.caf.rpc.api.serialize.RpcSerializeUtil;
import io.iec.edp.caf.rpc.api.service.InternalServiceManageService;
import io.iec.edp.caf.rpc.api.support.ConstanceVarible;
import io.iec.edp.caf.rpc.api.support.RpcThreadCacheHolder;
import io.iec.edp.caf.rpc.api.support.Type;
import io.iec.edp.caf.rpc.api.utils.Validator;
import io.iec.edp.caf.rpc.client.local.RpcLocalInvoker;
import io.iec.edp.caf.rpc.remote.http.discover.RpcAddressDiscover;
import io.iec.edp.caf.rpc.server.grpc.RpcRemoteInvokerPlus;
import io.iec.edp.caf.rpc.server.invoker.RpcRemoteInvoker;
import io.iec.edp.caf.tenancy.api.ITenantRouteService;
import io.iec.edp.caf.tenancy.api.ITenantService;
import io.iec.edp.caf.tenancy.api.context.RequestTenantContextHolder;
import io.iec.edp.caf.tenancy.api.context.RequestTenantContextInfo;
import io.iec.edp.caf.tenancy.api.entity.TenancyMode;
import io.iec.edp.caf.tenancy.api.exception.TenantNotFoundException;
import lombok.extern.slf4j.Slf4j;
import lombok.var;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.LinkedHashMap;

@Slf4j
public class GrpcClientImpl implements GrpcClient {

    private ServiceUnitAwareService serviceUnitAware;

    private InternalServiceManageService management;

    private RpcLocalInvoker rpcLocalInvoker;

    private RpcRemoteInvokerPlus rpcRemoteInvoker;
    private ITenantService tenantService;
    private ITenantRouteService tenantRouteService;

    private RpcAddressDiscover rpcAddressDiscover;

    private String cookieName;

    public GrpcClientImpl(InternalServiceManageService management, RpcLocalInvoker rpcLocalInvoker,
                          RpcRemoteInvokerPlus rpcRemoteInvoker, ServiceUnitAwareService serviceUnitAware, ITenantService tenantService,
                         ITenantRouteService tenantRouteService, RpcAddressDiscover rpcAddressDiscover) {


        this.management = management;
        this.rpcRemoteInvoker = rpcRemoteInvoker;
        this.rpcLocalInvoker = rpcLocalInvoker;
        this.serviceUnitAware = serviceUnitAware;
        this.cookieName = CafEnvironment.getEnvironment().getProperty("caf-security.general.cookie-name");
        this.tenantService = tenantService;
        this.tenantRouteService = tenantRouteService;
        this.rpcAddressDiscover = rpcAddressDiscover;
        if (this.cookieName == null) {
            this.cookieName = ConstanceVarible.COOKIE_SESSION_NAME;
        }
    }

    @Override
    public <T> T invoke(Class<T> clazz, String serviceId, String serviceUnitName, LinkedHashMap<String, Object> parameters, HashMap<String, String> context) {
        return this.invoke(new Type<>(clazz), serviceId, serviceUnitName, parameters, context);

    }

    @Override
    public <T> T invoke(Type<T> t, String serviceId, String serviceUnitName, LinkedHashMap<String, Object> parameters, HashMap<String, String> context) {
        try {
            //get cookies
            Cookie[] cks = this.getCookies();
            //check cookie only log warning not break invoke
            Validator.CheckCksSession(cks,serviceId);

            T result;
            //get rpc service definition from local memory variable
            RpcServiceMethodDefinition serviceMethodDefinition = this.management.getRpcMethodDefinition(serviceId);
            //get extend tenant id
            Integer targetTenantId = getTargetTenantId(serviceUnitName, context);

            //rpc service event context
            HashMap<String, String> eventContext = new HashMap<>();
            eventContext.put(ConstanceVarible.CURRENT_SERVICE_ID, serviceId);
            eventContext.put(ConstanceVarible.TARGET_SU,serviceUnitName);

            log.info("start rpc invoke，current serviceId：{}，current target su:{}",serviceId,serviceUnitName);
            Object retvalue = null;

            //is local invoke
            if (isLocalInvoke(serviceUnitName)) {
                //local memory has no rpc service definition
                if (serviceMethodDefinition == null) {
                    log.error("can not find rpc service by serviceid:"+serviceId+"，can not local invoke,current su："+serviceUnitName);
                    throw new CAFRuntimeException(DefaultExceptionProperties.SERVICE_UNIT,
                            DefaultExceptionProperties.RESOURCE_FILE,
                            ExceptionErrorCode.localServiceUnitNotFound,
                            new String[]{serviceId, serviceUnitName},
                            null, ExceptionLevel.Error, false);
                }

                eventContext.put(ConstanceVarible.IS_LOCAL, "true");

                if(log.isInfoEnabled()){
                    log.info("rpc local invoke info：", JSONSerializer.serialize(serviceMethodDefinition));
                }

                result = this.rpcLocalInvoker.invokeLocalService(t.getRawType(), serviceId, serviceUnitName, parameters, serviceMethodDefinition, targetTenantId,eventContext);

                RpcReturnValueDefinition returnValueDefinition = serviceMethodDefinition == null ? null : serviceMethodDefinition.getReturnInfo();
                retvalue = RpcSerializeUtil.deSerializeReturnValue(t, RpcSerializeUtil.serializeReturnValue(result, returnValueDefinition), serviceMethodDefinition);


            } else {
                //remote invoke
                try{
                    String remoteUrl = this.rpcAddressDiscover.getAddress(serviceUnitName+"_grpc",eventContext);

                    eventContext.put(ConstanceVarible.IS_LOCAL, "false");
                    eventContext.put(ConstanceVarible.CURRENT_REMOTEBASE_URL,remoteUrl);

                    log.info("rpc remote invoke baseurl：{}",remoteUrl);
                    retvalue = this.rpcRemoteInvoker.invokeRemoteService(t, serviceId, serviceUnitName, remoteUrl, parameters, cks, serviceMethodDefinition, targetTenantId,eventContext);

                }catch (ServiceUnitNotFoundException e){
                    //if not open grpc port
                    String remoteUrl = this.rpcAddressDiscover.getAddress(serviceUnitName,eventContext);
                    SpringBeanUtils.getBean(RpcRemoteInvoker.class).invokeRemoteService(t, serviceId, serviceUnitName, remoteUrl, parameters, cks, serviceMethodDefinition, targetTenantId,eventContext);
                }
            }

            return (T)retvalue;
        } catch (Exception e) {

            throw new RuntimeException(e);
        }finally {
            RpcThreadCacheHolder.clear();
        }
    }

    //铁远程
    @Override
    public <T> T invoke(Type<T> t, String serviceId, String serviceUnitName, LinkedHashMap<String, Object> parameters, HashMap<String, String> context, String remoteUrl) {
        return null;
    }

    @Override
    public String invoke(String serviceId, String serviceUnitName, LinkedHashMap<String, Object> parameters, HashMap<String, String> context) {
        return this.invoke(String.class, serviceId, serviceUnitName, parameters, context);
    }

    /**
     * get cookie
     * @return if the request has cookie and has caf session,then use the request cookie
     *         if request has no cookie then use {@link CAFContext} session create a new cookie
     * @throws UnsupportedEncodingException
     */
    private Cookie[] getCookies() throws UnsupportedEncodingException {
        Cookie[] cks = null;
        boolean containSessionId = false;
        //get request cookies
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        var sessionid = "";
        if (servletRequestAttributes != null) {
            HttpServletRequest request = servletRequestAttributes.getRequest();
            cks = request.getCookies();
            //if cookie has session,then use cookie session
            if (cks != null && cks.length > 0) {
                for (Cookie cookie : cks) {
                    if (cookie.getName().equalsIgnoreCase(this.cookieName)) {
                        containSessionId = true;
                        sessionid = cookie.getValue();
                        break;
                    }
                }
            }
        }
        var sessionsrv = SpringBeanUtils.getBean(ICafSessionService.class);

        if(log.isInfoEnabled()){
            log.info("rpc invoke requst has cookie:"+containSessionId);
            log.info("rpc get sessionid:"+ (sessionid != null ? new String(Base64.getDecoder().decode(sessionid), StandardCharsets.UTF_8) : sessionid)+" from cookie");
            log.info("rpc get sessionid from cookie isExpired:"+ (!"".equals(sessionid) ? sessionsrv.isExpired(new String(Base64.getDecoder().decode(sessionid), StandardCharsets.UTF_8)) : true));
            log.info("rpc get sessionid:"+CAFContext.current.getSessionId()+" from cafcontext");
            log.info("rpc get sessionid from cafcontext isExpired："+((CAFContext.current.getSessionId()!=null&&!"".equals(CAFContext.current.getSessionId())) ? sessionsrv.isExpired(CAFContext.current.getSessionId()) : true));
        }

        if(!containSessionId){
            cks = new Cookie[1];
            cks[0] = new Cookie(cookieName, base64Encode(CAFContext.current.getSessionId()));
        }
        return cks;
    }
    private String base64Encode(String value) throws UnsupportedEncodingException {

        if (org.springframework.util.StringUtils.isEmpty(value)) {
            return "";
        }

        byte[] encodedCookieBytes = Base64.getEncoder().encode(value.getBytes("UTF-8"));
        return new String(encodedCookieBytes);
    }

    /**
     * check is local invoke
     * @param serviceUnitName msu code
     * @return true:local invoke,false: remote invoke
     */
    private boolean isLocalInvoke(String serviceUnitName) {
        return this.serviceUnitAware.getEnabledServiceUnits().stream().anyMatch(d -> d.equalsIgnoreCase(serviceUnitName));
    }

    /**
     * get extend tenantid to route
     * by implement {@link ITenantRouteService} interface
     * @param msu msu code
     * @param context extend parameters to choose route tenant
     * @return if {@link ITenantRouteService} implement it will reture the ITenantRouteService`s tenantid,if not implement
     *          {@link ITenantRouteService} it will found tenantid in http header by CAFContextFilter,if both not found it
     *          will return null
     */
    private Integer getTargetTenantId(String msu, HashMap<String, String> context) {

        if (this.tenantService.getTenancyMode() != TenancyMode.group)
            return null;

        context = context == null ? new HashMap<>() : context;
        String tenantIdString = context.get(ConstanceVarible.ROUTE_TENANTID);
        if (tenantIdString != null) {
            return Integer.valueOf(tenantIdString);
        } else {
            String tenantDim1 = context.get(ConstanceVarible.ROUTE_FIRSTDIMENSION_KEY);
            String tenantDim2 = context.get(ConstanceVarible.ROUTE_SECONDDIMENSION_KEY);
            if (!StringUtils.isEmpty(tenantDim1)) {
                try {
                    return tenantRouteService.route(msu, tenantDim1, tenantDim2);
                } catch (TenantNotFoundException e) {
                    log.error(String.format("tenant route failed su：%s first dimension：%s ,second dimension：%s", msu, tenantDim1, tenantDim2));
                }
            }

            //get requset header tenant if set
            RequestTenantContextInfo contextInfo = RequestTenantContextHolder.get();
            return contextInfo == null ? null : contextInfo.getTenantId();
        }
    }
}
